package com.pi.shisan.service.audio;

import com.pi.shisan.service.util.ComponentManager;
import de.quippy.javamod.multimedia.ogg.metadata.OggMetaData;
import de.quippy.ogg.jogg.Packet;
import de.quippy.ogg.jogg.Page;
import de.quippy.ogg.jogg.StreamState;
import de.quippy.ogg.jogg.SyncState;
import de.quippy.ogg.jorbis.Block;
import de.quippy.ogg.jorbis.Comment;
import de.quippy.ogg.jorbis.DspState;
import de.quippy.ogg.jorbis.Info;

import javax.sound.sampled.AudioFormat;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class OggPlayer extends AudioPlayer {

    private InputStream inputStream;

    private static final int STATE_INITIAL = 0;
    private static final int STATE_READHEADER = 1;
    private static final int STATE_PREPARE = 2;
    private static final int STATE_READFIRSTFRAME = 3;
    private static final int STATE_PROCESSPACKET = 4;
    private static final int STATE_NEEDMOREDATA = 5;
    private static final int STATE_CONVERTPCM = 6;
    private static final int STATE_EOS = 7;

    private static final int CHUNKSIZE = 4096;

    private boolean oggEOS;
    private int decoderState;

    private SyncState oggSyncState;
    private StreamState oggStreamState;
    private Page oggPage;
    private Packet oggPacket;
    private Info vorbisInfo;
    private Comment vorbisComment;
    private DspState vorbisDSPState;
    private Block vorbisBlock;

    private AudioFormat audioFormat;

    private final float[][][] pcmFloatBuffer = new float[1][][];
    private int[] pcmGeneratorIndex;

    private int bufferSize;
    private int samplesProcessed;

    private long currentSamplesWritten;

    public OggPlayer(File audioFile, AudioEffectComponentsSupplier<ComponentManager.MusicEffectBaseComponent> musicEffectComponentsSupplier) throws Exception {
        super(audioFile, musicEffectComponentsSupplier);
        OggMetaData oggMetaData = new OggMetaData(audioFile);
        totalMilliseconds = oggMetaData.getLengthInMilliseconds();
        inputStream = new FileInputStream(audioFile);
    }

    @Override
    public void doDestroy() {
        clean();
    }

    @Override
    protected boolean doingPlay() throws Exception {
        int len;
        while ((len = decodeFrame()) > 0) {
            if (audioFormat != null) {
                if (audioOutputStream == null) {
                    openSound(audioFormat);
                }
                writeSampleData(getOutputBuffer(0), 0, len);
                if (playerStatus.get() != PLAYING) {
                    return false;
                }
            }
        }
        return true;
    }

    @Override
    protected void reset() throws Exception {
        clean();
        inputStream = new FileInputStream(audioFile);
        decoderState=STATE_INITIAL;
        oggEOS = false;
        bufferSize = 0;
        currentSamplesWritten = 0;
    }

    private void clean() {
        try {
            if (oggStreamState != null) {
                oggStreamState.clear();
                oggStreamState = null;
            }
        } catch (Exception e) {
        }
        try {
            if (vorbisBlock != null) {
                vorbisBlock.clear();
                vorbisBlock = null;
            }
        } catch (Exception e) {
        }
        try {
            if (vorbisDSPState != null) {
                vorbisDSPState.clear();
                vorbisDSPState = null;
            }
        } catch (Exception e) {
        }
        try {
            if (vorbisInfo != null) {
                vorbisInfo.clear();
                vorbisInfo = null;
            }
        } catch (Exception e) {
        }
        try {
            if (oggSyncState != null) {
                oggSyncState.clear();
                oggSyncState = null;
            }
        } catch (Exception e) {
        }
        if (inputStream != null) {
            try {
                inputStream.close();
                inputStream = null;
            } catch (Exception e) {
            }
        }
    }

    public int decodeFrame() throws Exception {
        while (true) {
            switch (decoderState) {
                case STATE_EOS:
//					if (inputStream.available()>0) // Are we streaming?!
//					{
//						decoderState = STATE_INITIAL;
//						OggMetaData metaData = new OggMetaData(inputStream);
//						break;
//					}
//					else
                    return -1;
                case STATE_INITIAL:
                    decoderState = doStateInitial();
                    break;
                case STATE_READHEADER:
                    decoderState = doStateReadHeader();
                    break;
                case STATE_PREPARE:
                    decoderState = doStatePrepare();
                    break;
                case STATE_READFIRSTFRAME:
                    decoderState = STATE_NEEDMOREDATA;
                    break;
                case STATE_NEEDMOREDATA:
                    decoderState = doStateNeedMoreData();
                    break;
                case STATE_PROCESSPACKET:
                    decoderState = doStateProcessPacket();
                    break;
                case STATE_CONVERTPCM:
                    decoderState = doStateConvertPCM();
                    if (decoderState == STATE_CONVERTPCM)
                        return samplesProcessed * 2 * vorbisInfo.channels;
                    break;
                default:
                    throw new IOException("invalid decoder state " + decoderState);
            }
        }
    }

    private int doStateInitial() throws Exception {
        oggSyncState = new SyncState();
        oggStreamState = new StreamState();
        oggPage = new Page();
        oggPacket = new Packet();

        vorbisInfo = new Info();
        vorbisComment = new Comment();
        vorbisDSPState = new DspState();
        vorbisBlock = new Block(vorbisDSPState);

        oggSyncState.init();
        oggEOS = false;

        return STATE_READHEADER;
    }

    private int doStateReadHeader() throws Exception {
        fetchMoreData();
        if (oggSyncState.pageout(oggPage) != 1) {
            throw new IOException("Input does not appear to be an Ogg bitstream");
        }
        oggStreamState.init(oggPage.serialno());
        oggStreamState.reset();
        if (oggStreamState.pagein(oggPage) < 0) {
            throw new IOException("Error reading first page of Ogg bitstream data");
        }
        if (oggStreamState.packetout(oggPacket) != 1) {
            // no page? must not be vorbis
            throw new IOException("Error reading initial header packet");
        }
        vorbisInfo.init();
        vorbisComment.init();
        if (vorbisInfo.synthesis_headerin(vorbisComment, oggPacket) < 0) {
            // error case; not a vorbis header
            throw new IOException("This Ogg bitstream does not contain Vorbis audio data");
        }
        int i = 0;
        while (i < 2) {
            while (i < 2) {
                int result = oggSyncState.pageout(oggPage);
                if (result == 0) {
                    break; // Need more data
                } else if (result == 1) {
                    oggStreamState.pagein(oggPage);
                    while (i < 2) {
                        result = oggStreamState.packetout(oggPacket);
                        if (result == 0) break;
                        if (result == -1) {
                            throw new IOException("Corrupt secondary header");
                        }
                        vorbisInfo.synthesis_headerin(vorbisComment, oggPacket);
                        i++;
                    }
                } else {
                    throw new IOException("Unhandled pageout() return code " + result);
                }
            }
            fetchMoreData();
            if (oggEOS) {
                throw new IOException("End of file before finding all Vorbis headers");
            }
        } // while i < 2
        return STATE_PREPARE;
    }

    private int doStateNeedMoreData() throws Exception {
        if (oggEOS) {
            return STATE_EOS;
        }
        while (!oggEOS) {
            int result;
            do {
                result = oggSyncState.pageout(oggPage);
                if (result == 0) {
                    fetchMoreData();
                }
            }
            while (!oggEOS && result == 0);

            if (result == -1) {
                // missing or corrupt data at this page
                continue;
            }

            oggStreamState.pagein(oggPage);
            if (oggPage.granulepos() == 0) {
                oggEOS = true;
                return STATE_EOS;
            }
            break;
        }
        return STATE_PROCESSPACKET;
    }

    private int doStateProcessPacket() throws Exception {
        int nextState = STATE_PROCESSPACKET;
        int result = oggStreamState.packetout(oggPacket);
        if (result == 0) {
            if (!oggEOS) {
                oggEOS = oggPage.eos() != 0;
            }
            nextState = STATE_NEEDMOREDATA;
        } else if (result == -1) {
            // missing or corrupt data at this page position
        } else {
            // we have a packet. Decode it
            if (vorbisBlock.synthesis(oggPacket) == 0) { // test for success!
                vorbisDSPState.synthesis_blockin(vorbisBlock);
            }
            nextState = STATE_CONVERTPCM;
        }
        return nextState;
    }

    private int doStateConvertPCM() throws Exception {
        int nextState = STATE_PROCESSPACKET;
        int samplesGenerated = vorbisDSPState.synthesis_pcmout(pcmFloatBuffer, pcmGeneratorIndex);
        if (samplesGenerated > 0) {
            samplesProcessed = (samplesGenerated > bufferSize) ? bufferSize : samplesGenerated;
            for (int i = 0; i < vorbisInfo.channels; i++) {
                int sampleIndex = i << 1;

                // For every sample in our range...
                for (int j = 0; j < samplesProcessed; j++) {
                    int value = (int) (pcmFloatBuffer[0][i][pcmGeneratorIndex[i] + j] * 32767);

                    if (value > 32767) value = 32767;
                    else if (value < -32768) value = -32768;

                    if (value < 0) value |= 0x8000;

                    getOutputBuffer(0)[sampleIndex] = (byte) (value & 0xFF);
                    getOutputBuffer(0)[sampleIndex + 1] = (byte) ((value >> 8) & 0xFF);

                    sampleIndex += vorbisInfo.channels << 1;
                }
            }
            currentSamplesWritten += samplesProcessed;
            if (vorbisInfo.rate!=0)
                currentMilliseconds = currentSamplesWritten * 1000L / (long)vorbisInfo.rate;
            else
                currentMilliseconds = 0;
            vorbisDSPState.synthesis_read(samplesProcessed);
            nextState = STATE_CONVERTPCM;
        }
        return nextState;
    }

    private final int doStatePrepare() throws Exception {
        vorbisDSPState.synthesis_init(vorbisInfo);
        vorbisBlock.init(vorbisDSPState);

        pcmGeneratorIndex = new int[vorbisInfo.channels];

        currentSamplesWritten = 0;

        // create SampleBuffer for output
        bufferSize = 250 * vorbisInfo.channels * vorbisInfo.rate / 1000; // 250ms buffer

        // Now for the bits (linebuffer):
        bufferSize <<= 1;
        getOutputBuffer(bufferSize);

        audioFormat = new AudioFormat((float) vorbisInfo.rate, 16, vorbisInfo.channels, true, false);

        return STATE_READFIRSTFRAME;
    }

    private void fetchMoreData() throws IOException {
        if (!oggEOS) {
            final int oggIndex = oggSyncState.buffer(CHUNKSIZE);
            final int bytesRead = inputStream.read(oggSyncState.data, oggIndex, CHUNKSIZE);
            if (bytesRead <= 0) {
                oggEOS = true;
            } else {
                oggSyncState.wrote(bytesRead);
            }
        }
    }
}
